"""Grafana objects."""
import copy
import functools
import os
import pathlib
import shutil
from urllib import parse

from cki_lib import logger
from cki_lib import misc
from cki_lib import session
from cki_lib import yaml
from requests.exceptions import HTTPError

LOGGER = logger.get_logger(__name__)

GRAFANA_URL = os.environ.get('GRAFANA_URL')
GRAFANA_TOKEN = os.environ.get('GRAFANA_TOKEN')

SESSION = session.get_session(__name__, raise_for_status=True)
SESSION.headers.update({'Authorization': f'Bearer {GRAFANA_TOKEN}'})


class API:
    # pylint: disable=too-few-public-methods
    """API requests."""

    @staticmethod
    def _get(endpoint):
        """GET request and return result."""
        return SESSION.get(parse.urljoin(GRAFANA_URL, endpoint)).json()

    @staticmethod
    def _post(endpoint, data):
        """POST request."""
        if misc.is_production():
            SESSION.post(parse.urljoin(GRAFANA_URL, endpoint), json=data)
        else:
            LOGGER.warning('Would post to %s with %s', endpoint, data)

    @staticmethod
    def _put(endpoint, data):
        """PUT request."""
        if misc.is_production():
            SESSION.put(parse.urljoin(GRAFANA_URL, endpoint), json=data)
        else:
            LOGGER.warning('Would post to %s with %s', endpoint, data)


class Object(API):
    """Generic Grafana object."""

    _name = None
    _uid = None

    _url = None
    _identifier = None

    def __init__(self, path):
        """Init."""
        self.path = pathlib.Path(path, self._name)

    def _save(self, data):
        """Save the object to the filesystem."""
        file_name = f'{misc.get_nested_key(data, self._uid)}.yml'.replace(' ', '_')
        file_path = pathlib.Path(self.path, file_name)
        file_path.parent.mkdir(parents=True, exist_ok=True)

        LOGGER.info('%s: Saving: %s', self._name, file_path)
        file_path.write_text(yaml.dump(data), encoding='utf8')

    def _get_element(self, element):
        # pylint: disable=no-self-use
        """Perform some action with element if necessary."""
        return element

    def _remote_exists(self, obj):
        """Return True if the object exists on the server."""
        endpoint = self._url + self._identifier.format(**obj)
        try:
            self._get(endpoint)
            return True
        except HTTPError:
            return False

    def _remote_update(self, obj):
        """Update remote copy of this object."""
        LOGGER.info('%s: Updating %s', self._name, misc.get_nested_key(obj, self._uid))

        endpoint = self._url + self._identifier.format(**obj)
        self._put(endpoint, obj)

    def _remote_create(self, obj):
        """Create new object in the server."""
        LOGGER.info('%s: Creating %s', self._name, misc.get_nested_key(obj, self._uid))

        self._post(self._url, obj)

    def delete_all(self):
        """Delete all local storage."""
        LOGGER.info('%s: Deleting local copy: %s', self._name, self.path)
        if self.path.is_dir():
            shutil.rmtree(self.path)

    def download_all(self):
        """Download and locally store all objects."""
        LOGGER.info('%s: Downloading all to: %s', self._name, self.path)

        all_elements = self._get(self._url)
        for element in all_elements:
            element = self._get_element(element)
            self._save(element)

    def upload_all(self):
        """Update if exists, otherwise create."""
        files = pathlib.Path(self.path).glob('*.yml')
        for file in files:
            obj = yaml.load(file_path=file)
            if self._remote_exists(obj):
                self._remote_update(obj)
            else:
                self._remote_create(obj)


class Datasource(Object):
    """Datasource."""

    _name = 'datasource'
    _uid = 'id'

    _url = '/api/datasources'
    _identifier = '/{id}'


class Folder(Object):
    """Folder."""

    _name = 'folder'
    _uid = 'uid'

    _url = '/api/folders'
    _identifier = '/{uid}'

    def _remote_update(self, obj):
        """Update folder."""
        super()._remote_update(dict(obj, overwrite=True))


class Dashboard(Object):
    """Dashboard."""

    _name = 'dashboard'
    _uid = 'dashboard/uid'
    _url = '/api/search?type=dash-db'

    def _get_element(self, element):
        return self._get(f'/api/dashboards/uid/{element["uid"]}')

    def _remote_exists(self, _):
        """
        Return True.

        Dashboards needs to be always updated, no matter if they exist or not.
        """
        return True

    @classmethod
    @functools.lru_cache
    # lru_cache is ok here as static/class methods don't suffer from the issues described at
    # https://rednafi.github.io/reflections/dont-wrap-instance-methods-with-functoolslru_cache-decorator-in-python.html
    def _folder_id(cls, title):
        """Lookup a folder by name."""
        return next((f['id'] for f in cls._get('/api/folders')
                     if f['title'] == title), 0)

    def _remote_update(self, obj):
        """Update or create dashboard."""
        LOGGER.info('%s: Updating %s', self._name, obj['dashboard']['title'])
        obj = {
            'dashboard': copy.deepcopy(obj['dashboard']),
            'folderId': self._folder_id(obj['meta']['folderTitle']),
            'overwrite': True,
        }
        # Delete ID so it creates a new one if not present.
        del obj['dashboard']['id']
        self._post('/api/dashboards/db', obj)


ALL_OBJECTS = (
    Datasource,
    Folder,
    Dashboard,
)
